<?php
class UrlRouter {
	private $routes;
	private $inverseRoutes;
	private $indexes;
	private $page404;
	private $redirect;
	private $rootdir;
	private $rooturl;
	private $path;
	private $query;
	private $baseurl;
	private $buffer;
	
	function __construct($config) {
		$this->routes = $config['routes'];
		$this->calculateInverseRoutes();
		$this->indexes = $config['indexes'];
		$this->page404 = $config['page404'];
		$this->redirect = $config['redirect'];
		$this->rootdir = dirname(__FILE__).'/';
		$context = dirname($_SERVER['PHP_SELF']);
		if ($context != '/') $context .= '/';
		$this->rooturl = 'http://'.$_SERVER['SERVER_NAME'].$context;
		list($this->path, $this->query) = $this->parseUrl($_SERVER['REQUEST_URI']);
		$this->path = preg_replace('~^'.preg_quote($context, '~').'~', '', $this->path);
		$this->baseurl = dirname($this->rooturl.$this->path).'/';
	}
	
	function run() {
		if (!isset($this)) {
			$router = new UrlRouter(require(dirname(__FILE__).'/router-config.php'));
			return $router->run();
		}
		if (!$this->validatePath()) $this->path = $this->page404;
		if ($this->redirect) $this->applyRedirect();
		$this->applyRoutes();
		if ($this->is404()) $this->send404();
		if ($this->isScript()) {
			$this->runScript();
		} elseif ($this->isHtmlFile()) {
			$this->sendHtmlFile();
		} else {
			$this->sendFile();
		}
	}
	
	private function parseUrl($url) {
		preg_match('~^([^\\?#]*)(?:\\?([^#]*))?(?:#(.*))?$~', $url, $match);
		return array(
			$match[1],
			isset($match[2]) ? $match[2] : null,
			isset($match[3]) ? $match[3] : null,
		);
	}
	
	private function buildUrl($path, $query = null, $fragment = null) {
		$url = $path;
		if ($query) $url .= '?'.$query;
		if ($fragment) $url .= '#'.$fragment;
		return $url;
	}
	
	private function parseQuery($query) {
		preg_match_all('~([^=&]+)(?:=([^&]*))?~', (string) $query, $matches);
		return $matches[1] ? array_combine(array_map('urldecode', $matches[1]), array_map('urldecode', $matches[2])) : array();
	}
	
	private function buildQuery($query) {
		$querystr = array();
		foreach ($query as $name => $value) {
			$querystr[] = rawurlencode($name).($value ? '='.rawurlencode($value) : '');
		}
		return implode('&', $querystr);
	}
	
	private function absoluteUrl($url) {
		if (preg_match('~^http://~i', $url)) return $url;
		return $this->baseurl.$url;
	}
	
	private function calculateInverseRoutes() {
		$this->inverseRoutes = array();
		foreach ($this->routes as $pattern => $route) {
			$this->calculateInverseRoute($pattern, $route);
		}
	}
	
	private function calculateInverseRoute($pattern, $route) {
		list($path, $query) = $this->parseUrl($route);
		$query = $this->parseQuery($query);
		$pattern = preg_split('~\\(([^\\)]*)\\)~', $pattern, -1, PREG_SPLIT_DELIM_CAPTURE);
		foreach ($query as $name => $value) {
			if (preg_match('~^\\$([0-9]+)$~', $value, $match)) {
				$index = $match[1] * 2 - 1;
				$query[$name] = $pattern[$index];
				$query[$name] = '~^'.str_replace('~', '\\~', $pattern[$index]).'$~';
				$pattern[$index] = $name;
			} else {
				$query[$name] = '~^'.preg_quote($value, '~').'$~';
			}
		}
		$this->inverseRoutes[] = compact('path', 'query', 'pattern');
	}
	
	private function applyInverseRoutes($url) {
		$url = $this->absoluteUrl($url);
		if (preg_match('~^'.preg_quote($this->rooturl, '~').'(.*)$~', $url, $match)) {
			list($path, $query, $fragment) = $this->parseUrl($match[1]);
			$query = $this->parseQuery($query);
			foreach ($this->inverseRoutes as $route) {
				if ($path != $route['path']) continue;
				foreach ($route['query'] as $name => $validation) {
					if (!isset($query[$name])) continue 2;
					if (!preg_match($validation, $query[$name])) continue 2;
				}
				$path = '';
				foreach ($route['pattern'] as $i => $token) {
					if ($i % 2) {
						$path .= rawurlencode($query[$token]);
						unset($query[$token]);
					} else {
						$path .= $token;
					}
				}
				return $this->baseurl.$this->buildUrl($path, $this->buildQuery($query), $fragment);
			}
		}
		return $url;
	}
	
	private function applyRoutes() {
		foreach ($this->routes as $pattern => $route) {
			$url = preg_replace('~^'.str_replace('~', '\\~', $pattern).'$~', $route, $this->path, 1, $match);
			if ($match) {
				list($this->path, $query) = $this->parseUrl($url);
				if ($query) $this->query .= ($this->query ? '&' : '') . $query;
				break;
			}
		}
		if (is_dir($this->rootdir.$this->path)) {
			$dir = rtrim($this->path, '/');
			if ($dir) $dir .= '/';
			foreach ($this->indexes as $index) {
				$this->path = $dir.$index;
				if (file_exists($this->rootdir.$this->path)) break;
			}
		}
		if (!file_exists($this->rootdir.$this->path) || is_dir($this->rootdir.$this->path)) {
			$this->path = $this->page404;
		}
		$this->baseurl = dirname($this->rooturl.$this->path).'/';
	}
	
	private function applyRedirect() {
		$url = $this->buildUrl($this->baseurl.$this->path, $this->query);
		$inverseUrl = $this->applyInverseRoutes($url);
		if ($inverseUrl != $url) {
			header('HTTP/1.0 301 Moved Permanently');
			header('Location: '.$inverseUrl);
			die;
		}
	}
	
	private function validatePath() {
		if (preg_match('~(^|/)\\.+($|/)~', $this->path)) return false;
		return true;
	}
	
	private function isScript() {
		if (preg_match('~\\.php$~i', $this->path)) return true;
		return false;
	}
	
	private function runScript() {
		$this->simulateQueryString();
		if (ini_get('register_globals')) $this->simulateRegisterGlobals();
		chdir(dirname($this->rootdir.$this->path));
		extract($GLOBALS, EXTR_REFS);
		$this->startBuffer();
		require $this->rootdir.$this->path;
		$this->endBuffer();
	}
	
	private function simulateQueryString() {
		$_GET = array();
		parse_str($this->query, $_GET);
		$G = $_GET;
		$P = $_POST;
		$C = $_COOKIE;
		$_REQUEST = call_user_func_array('array_merge', compact(str_split(ini_get('variables_order'))));
	}
	
	private function simulateRegisterGlobals() {
		$E = $_ENV;
		$G = $_GET;
		$P = $_POST;
		$C = $_COOKIE;
		$S = $_SERVER;
		$GLOBALS = array_merge(
			array('GLOBALS' => &$GLOBALS),
			call_user_func_array('array_merge', compact(str_split(ini_get('variables_order')))),
			array(
				'_POST' => $_POST,
				'_GET' => $_GET,
				'_COOKIE' => $_COOKIE,
				'_SERVER' => $_SERVER,
				'_ENV' => $_ENV,
				'_FILES' => $_FILES,
				'_REQUEST' => $_REQUEST
			)
		);
	}
	
	private function isHtmlFile() {
		if (preg_match('~\\.(html|htm)$~i', $this->path)) return true;
		return false;
	}
	
	private function sendHtmlFile() {
		$this->startBuffer();
		$file = fopen($this->rootdir.$this->path, 'r');
		while (!feof($file)) {
			echo fread($file, 512);
		}
		$this->endBuffer();
	}
	
	private function startBuffer() {
		$this->buffer = '';
		ob_start(array($this, 'outputFilter'), 2);
		ob_implicit_flush();
	}
	
	private function endBuffer() {
		ob_end_flush();
	}
	
	private function outputFilter($output, $mode) {
		$oldErrorReporting = error_reporting(0);
		$this->buffer .= $output;
		$output = '';
		$tokens = preg_split('~(<[^>]+(?:>|$))~', $this->buffer, -1, PREG_SPLIT_DELIM_CAPTURE);
		$this->buffer = '';
		foreach ($tokens as $i => $token) {
			if ($i % 2) {
				if (substr($token, -1) != '>') {
					$this->buffer = $token;
					break;
				}
				$token = $this->filterTag($token);
			}
			$output .= $token;
		}
		if ($mode & PHP_OUTPUT_HANDLER_END) {
			if ($this->buffer) {
				$output .= $this->filterTag($this->buffer);
				$this->buffer = '';
			}
		}
		error_reporting($oldErrorReporting);
		return $output;
	}
	
	private function filterTag($tag) {
		preg_match('~^<(\\S*)~', $tag, $match);
		switch(strtolower($match[1])) {
		case 'a':
		case 'link':
			return preg_replace_callback('~\\b(href=)("[^"]*"|\'[^\']*\'|[^\\s>]*)~', array($this, 'filterAttribute'), $tag);
		case 'script':
		case 'img':
			return preg_replace_callback('~\\b(src=)("[^"]*"|\'[^\']*\'|[^\\s>]*)~', array($this, 'filterAttribute'), $tag);
		default:
			return $tag;
		}
	}
	
	private function filterAttribute($match) {
		$url = preg_replace('~^("|\'|)(.*)\\1$~', '$2', $match[2]);
		$url = $this->applyInverseRoutes($url);
		return $match[1].'"'.$url.'"';
	}
	
	private function sendFile() {
		readfile($this->rootdir.$this->path);
	}
	
	private function is404() {
		return $this->path == $this->page404;
	}
	
	private function send404() {
		header('HTTP/1.0 404 Not Found');
		if (!file_exists($this->rootdir.$this->path)) die('Page Not Found');
	}
}

UrlRouter::run();
